Skip to content

Roll TimeWithZones forward in DST-aware manner - #339

Merged
OJFord merged 2 commits into
masterfrom
timezone
Nov 3, 2026
Merged

OJFord merged 2 commits into
masterfrom
timezone

Conversation

@OJFord

@OJFord OJFord commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Presently, roll_forward, next_business_day, et al. use day_interval_for's definition of 1 (days) for Date objects, or 60 * 60 * 24 (seconds) otherwise.

Falling into the latter category, ActiveSupport::TimeWithZone objects in Rails environments have their date component adjusted in a manner which is not timezone-aware, and thus corrupts the time component when the change spans a DST transition. For example:

t = TimeWithZone.new(Time.new(2026,10,23,12,0,0), ActiveSupport::TimeZone::country_zones(:gb)[0])

constructs t as noon UTC in Europe/London while BST is active, i.e. t is 1pm local time.

next_business_day(t)

gives 2026-10-26 12:00:00.000000000 GMT +00:00, that is, still noon UTC, but now also noon local time, as the next business day was post transition to GMT.

This commit addresses the issue by detecting TimeWithZones, and using ActiveSupport's Integer#day as for the interval in such cases; in the above example giving 2026-10-26 13:00:00.000000000 GMT +00:00 as expected.

Presently, `roll_forward`, `next_business_day`, et al. use
`day_interval_for`'s definition of `1` (days) for `Date` objects, or
`60 * 60 * 24` (seconds) otherwise.

Falling into the latter category, `ActiveSupport::TimeWithZone` objects
in Rails environments have their date component adjusted in a manner
which is not timezone-aware, and thus corrupts the time component when
the change spans a DST transition. For example:

    t = TimeWithZone.new(Time.new(2026,10,23,12,0,0), ActiveSupport::TimeZone::country_zones(:gb)[0])

constructs `t` as noon UTC in `Europe/London` while BST is active, i.e.
`t` is 1pm local time.

    next_business_day(t)

gives `2026-10-26 12:00:00.000000000 GMT +00:00`, that is, still noon
UTC, but now also noon local time, as the next business day was post
transition to GMT.

This commit addresses the issue by detecting `TimeWithZone`s, and using
ActiveSupport's `Integer#day` as for the interval in such cases; in the
above example giving `2026-10-26 13:00:00.000000000 GMT +00:00` as
expected.

@loreenwong loreenwong left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a spec to test this across a DST boundary?

edit: Nevermind its here

@OJFord

OJFord commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, I didn't want to take on a dependency for it here, so test has limited utility but at least exercises the path.

@OJFord
OJFord merged commit 1f1a302 into master Nov 3, 2026
4 checks passed
@OJFord
OJFord deleted the timezone branch November 3, 2026 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants